<?php
namespace app\service;

use think\facade\Filesystem;
use think\facade\Request;
use think\Exception;

// 文件上传类
class Uploadservice
{
    protected $config;
    protected $tempDir = './uploads/temp/';  // 分片临时目录
    protected $baseDir = './uploads/';

    public function __construct($scene = 'default')
    {
        // 加载配置（支持场景化配置）
        $this->config = array_merge(
            config('upload.default'),
            config("upload.{$scene}", [])
        );
        // 确保目录存在
        $this->checkDir($this->config['save_path']);
        $this->checkDir($this->tempDir);
    }

    /**
     * 传统多文件上传（适合小文件）
     * @param string $field 表单文件字段名（如'files'）
     * @return array 统一结果
     */
    public function multiUpload(string $field = 'files'): array
    {
        $files = Request::file($field);
        if (empty($files)) {
            return $this->error('未上传文件');
        }

        $result = [];
        foreach ($files as $file) {
            try {
                $res = $this->uploadSingle($file);
                $resutl[] = $res;
            } catch (Exception $e) {
                return $this->error($e->getMessage());
            }
        }

        return $this->success('上传成功', $result);
    }

    /**
     * 检查已上传的分片，返回分片序号集合（如[1,2,3,5]）
     * @param string $identifier 文件唯一标识（前端传递）
     * @return array 已上传的分片序号数组（升序排列）
     */
    public function checkUploadedChunks(string $identifier): array
    {
        $chunkDir = $this->tempDir . $identifier . '/';

        // 检查临时目录是否存在
        if (!is_dir($chunkDir)) {
            return [];
        }

        $uploadedChunks = [];
        // 扫描目录下的所有文件（排除子目录）
        $files = scandir($chunkDir);
        foreach ($files as $file) {
            // 跳过非文件项（如...）
            if (!is_file($chunkDir . $file)) {
                continue;
            }

            // 解析分片序号，（文件名格式：{chunkNumber}_{originalName}）
            $parts = explode('_', $file, 2);    //分割为两部分（避免原文件名包含下划线）
            if (count($parts) !== 2) {
                continue;
            }

            $chunkNumber = intval($parts[0]);
            if ($chunkNumber >= 0) {   // 过滤非数字或0的序号
                $uploadedChunks[] = $chunkNumber;
            }
        }

        // 去重并按升序排序
        $uploadedChunks = array_unique($uploadedChunks);
        sort($uploadedChunks);

        return $uploadedChunks;
    }

    /**
     * 分片上传（适合大文件）
     * @param string $identifier 文件唯一标识（如MD5哈希，用于关联分片）
     * @return bool|string
     */
    public function chunkUpload($chunk, $chunkNumber, $totalChunks, $identifier)
    {
        $chunk = $chunk ?? Request::file('chunk');    // 分片文件（前端传递的字段）
        $chunkNumber = $chunkNumber ?? Request::param('chunknumber', 0, 'trim');  // 当前分片序号（从1开始）
        $totalChunks = $totalChunks ?? Request::param('totalchunks', 0, 'trim');  //总分片数
        $identifier = $identifier ?? Request::param('identifier', '', 'trim');  //文件唯一标识
        
        
        // 校验分片参数
        if (!$chunk || $chunkNumber < 0 || $totalChunks <= 0) {
            return '分片参数错误';
        }

        // 临时分片目录
        $chunkDir = $this->tempDir . $identifier . '/';
        $this->checkDir($chunkDir);

        // 保存当前分片
        $chunkPath = $chunkDir . $chunkNumber . '_' . $identifier;
        if (!$chunk->move(dirname($chunkPath), basename($chunkPath))) {
            return "分片{$chunkNumber}保存失败";
        }

        // 检查是否所有分片已上传（可选，用于提前合并）
        // if ($chunkNumber === $totalChunks) {
        //     return $this->mergeChunks($identifier, $totalChunks);
        // }

        return "分片保存成功";
    }

    /**
     * 合并分片
     */
    public function mergeChunks(string $identifier, int $totalChunks, string $filename, string $saveDir = ''): array
    {
        $identifier = $identifier ?? Request::param('identifier', '', 'trim');
        $totalChunks = $$totalChunks ?? Request::param('totalchunks', 0, 'trim');
        $filename = $$filename ?? Request::param('filename', '', 'trim');
        
        $chunkDir = $this->tempDir . $identifier . '/';

        $saveDir = $saveDir ?? $this->baseDir . $identifier . '/';
        // 确保最终保存文件夹存在
        $this->checkDir($saveDir);

        $savePath =  $saveDir  . $filename;

        
        // 检查所有分片是否存在
        for ($i = 0; $i <= $totalChunks - 1; $i++) {
            $chunkPath = $chunkDir . $i . '_' . $identifier; // 实际需匹配具体文件名
            if (!glob($chunkPath)) {
                return $this->error("分片 {$i} 缺失，合并失败");
            }
        }
       
        // 合并分片（按顺序拼接）
        $fp = fopen($savePath, 'wb');
        for ($i = 0; $i <= $totalChunks - 1; $i++) {
            $chunkPath = $chunkDir . $i . '_' . $identifier;
            fwrite($fp, file_get_contents($chunkPath));
            unlink($chunkPath); // 删除临时分片
        }
        fclose($fp);
        
        // 清理临时目录
        $this->deleteDir($chunkDir);

        $extension = pathinfo($savePath, PATHINFO_EXTENSION);
        
        $basename = basename($filename, '.' . $extension);
        $hash = bin2hex(random_bytes(16));

        $finallyPath = $saveDir . $hash . '.' . $extension;
        if(!rename($savePath, $finallyPath)){
            return $this->error('文件重命名失败');
        }
        
        $data = [
            'extension' => $extension,
            'basename' => $basename,
            'filename' => $hash . '.' . $extension,
            'size' => filesize($finallyPath),
            'savePath' => $finallyPath
        ];
        return $this->success('合并成功', $data);
    }

    /**
     * 上传单个文件（内部方法，供传统和分片模式调用）
     */
    protected function uploadSingle($file): array
    {
        // 校验文件大小（传统模式直接校验，分片模式总分片大小由前端控制）
        $fileSize = $file->getSize();
        if ($fileSize > $this->config['max_size']) {
            throw new Exception("文件大小超过限制（最大{$this->formatSize($this->config['max_size'])}）");
        }

        // 校验文件类型（扩展名或MIME）
        $ext = $file->getOriginalExtension();
        if (!in_array(strtolower($ext), explode(',', $this->config['ext']))) {
            throw new Exception("不允许的文件扩展名（仅支持{$this->config['ext']}）");
        }
        if (!empty($this->config['mime'])) {
            $mime = $file->getMimeType();
            if (!in_array($mime, $this->config['mime'])) {
                throw new Exception("不允许的文件类型（仅支持指定MIME类型）");
            }
        }

        // 自动重命名（防止路径遍历）
        if ($this->config['auto_rename']) {
            $newName = uniqid() . '.' . $ext;
        } else {
            $newName = $file->getOriginalName();    //需额外校验原始文件名安全性
        }

        // 保存文件
        $savePath = $this->savePath . $newName;
        if (!$file->move(dirname($savePath), basename($savePath))) {
            throw new Exception("文件保存失败");
        }

        return [
            'name' => $newName,
            'path' => $savePath,
            'size' => $fileSize,
            'ext' => $ext,
            'url' => $this->getUrl($savePath) // 可选：返回访问URL
        ];
    }

    /**
     * 工具方法：检查目录是否存在，不存在则创建
     */
    protected function checkDir(string $dir)
    {
        if (!is_dir($dir)) {
            try {
                mkdir($dir, 0755, true);
            } catch (Exception $e) {
                return $e;
            }
        }
    }

    /**
     * 工具方法：删除目录（递归）
     */
    protected function deleteDir(string $dir)
    {
        try{
        if (is_dir($dir)) {
            $files = scandir($dir);
            foreach ($files as $file) {
                if ($file != '.' && $file != '..') {
                    $path = $dir . $file;
                    is_dir($path) ? $this->deleteDir($path) : unlink($path);
                }
            }
            rmdir($dir);
        }
        }catch(Exception $e){
            return;
        }
    }

    /**
     * 工具方法：获取文件扩展名（含点）
     */
    protected function getFileExt(string $filename): string
    {
        return '.' . pathinfo($filename, PATHINFO_EXTENSION);
    }

    /**
     * 工具方法：格式化文件大小（如50MB）
     */
    protected function formatSize(int $bytes): string
    {
        $units = ['B', 'KB', 'MB', 'GB'];
        $unitIndex = 0;
        while ($bytes >= 1024 && $unitIndex < 3) {
            $bytes /= 1024;
            $unitIndex++;
        }
        return number_format($bytes, 2) . $units[$unitIndex];
    }

    /**
     * 工具方法：获取文件访问URL（根据实际部署调整）
     */
    protected function getUrl(string $path): string
    {
        $publicPath = config('/');    //或直接写'/'
        return str_replace($publicPath, '', $path);
    }

    /**
     * 统一返回成功
     */
    protected function success(string $msg, $data = []): array
    {
        return [
            'status' => 1,
            'msg' => $msg,
            'data' => $data
        ];
    }

    /**
     * 统一返回错误
     */
    protected function error(string $msg): array
    {
        return [
            'status' => 0,
            'msg' => $msg,
            'data' => []
        ];
    }
}